﻿/*
 * Copyright 2006-2007 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package asphalt2.util{
	
	import asphalt2.util.Async;
	import flash.events.Event;
	import flash.events.EventDispatcher;
	import flash.utils.Dictionary;
	import flash.utils.setTimeout;
	
	import mx.effects.Effect;
	import mx.events.EffectEvent;

	[Event(name="complete", type="flash.events.Event")]
		
	/**
	 * タスクを順次実行するクラス
	 * addTaskで呼び出したいFunctionを渡すと、Taskを１つ追加します。
	 * デフォルトでは、タスクが終了してからtaskToTaskIntervalの間隔をあけて次のタスクを呼び出します。
	 * コンストラクタ引数でtrueを渡すと、直前のタスクが終了したかどうかに関わらずtaskToTaskIntervalの
	 * 間隔でFunctionを呼び出します。
	 * @author c9katayama
	 */
	public class $Chain extends EventDispatcher{
		/**
		 * 最初のメソッドを呼び出す間隔
		 */
		public var startInterval:uint;
		/**
		 * 処理と処理の間隔
		 */
		public var taskToTaskInterval:uint;
		/**
		 * 並列処理かどうか
		 * trueの場合、直前処理が終了しているかどうかに関わらず
		 * taskToTaskIntervalの間隔で処理を呼び出します。 
		 */
		public var concurrent:Boolean;

		//実行するタスクのリスト
		private var taskList:Array;

		/**
		 * TaskChainを作成します。 
		 * @param concurrent 並列実行するかどうか　デフォルトはfalse 
		 * @param taskToTaskInterval はじめのタスクが実行されるまでの時間 デフォルト０
		 * @param taskToTaskInterval　タスクとタスクの間の時間　デフォルト０
		 * 
		 **/
		public function $Chain(concurrent:Boolean=false,startInterval:uint=0,taskToTaskInterval:uint=0){
			taskList = new Array();		
			this.concurrent = concurrent;
			this.startInterval = startInterval;
			this.taskToTaskInterval = taskToTaskInterval;
		}
		public function $add(...chains):$Chain{
			for each(var chain:* in chains){
				if(chain == null){
					continue;
				}
				if(chain is Function){
					add(chain);
				}else if(chain is $Chain){
					addChain(chain);
				}else if(chain is Effect){
					addEffect(chain);
				}else if(chain in int){
					addWait(chain);
				}else{
					throw new ArgumentError(chain.toString()+" not support.");
				}
			}
			return this;
		}
		/**
		 * タスクを追加します。
		 * @param executeFunctions 実行するFunction(複数可）
		 **/
		public function add(chainFunction:Function):$Chain{
			_addTask(chainFunction);
			return this;
		}
		/**
		 * エフェクトを追加します。
		 * このメソッドでエフェクトを追加すると、Effect.play()を呼び出し、
		 * EffectEvent.EFFECT_ENDで次のタスクを呼び出す処理を行います。
		 * @param effects 実行するエフェクト（複数可）
		 **/
		public function addEffect(effect:Effect):$Chain{
			_addTask(_createEffectFunction(effect));
			return this;
		}
		protected function _createEffectFunction(effect:Effect):Function{
			var targetNum:int = effect.targets.length;
			return function(async:Async):void{
					effect.addEventListener(EffectEvent.EFFECT_END,
						function(e:Event):void{
							targetNum--;
							if(targetNum==0){						
								async.gotoNext();
							}
						}
					);
					if(!effect.isPlaying){
						effect.play();
					}
			}
		}
		/**
		 * 戻り値でエフェクトを返すFunctionを追加します。
		 * （例： function():Effect{
		 *      var fade:Face = new Fade(targetComponent);
		 *      fade.alphaFrom=0;
		 *      fade.alphaTo=1;
		 *      return fade;
		 *     }
		 * )
		 * このメソッドでFunctionを追加すると、戻り値のエフェクトのEffect.play()を呼び出し、
		 * EffectEvent.EFFECT_ENDで次のタスクを呼び出す処理を行います。
		 **/
		public function addEffectCreate(effectCreateFunction:Function):$Chain{
			_addTask(function(async:Async):void{
				var o:* = effectCreateFunction();
				if(!(o is Effect)){
					throw new Error("effectCreate return value is not Effect");
				}
				var effect:Effect = o as Effect;
				_createEffectFunction(effect)(async);
			});
			return this;
		}
		/**
		 * $Chain中に実行する$Chainを追加します。
		 * @param taskChains 実行するTaskChain(複数可）
		 **/
		public function addChain(chainChain:$Chain):$Chain{
			if(chainChain==this){
				throw new Error("Recursive chain not support.");
			}
			var f:Function = function(async:Async):void{
				chainChain.addEventListener(Event.COMPLETE,
					function():void{
						async.gotoNext();
					});
				chainChain.execute();
			}
			_addTask(f);
			return this;
		}		
		/**
		 * 次の処理開始を待つタスクを追加します。
		 * @param waitTime:処理待ちミリ時間（1秒=1000)
		 */
		public function addWait(waitTime:int):void{
			_addTask(function(async:Async):void{
				setTimeout(async.nextFunction,waitTime);
			});
		}
		/**
		 * タスクを追加します。
		 * @param executeFunction 実行するFunction
		 **/
		internal function _addTask(executeFunction:Function):void{
			if(executeFunction != null){
				taskList.push(executeFunction);
			}
		}
		/**
		 * タスクの実行
		 */
		public function execute():void{
			if(taskList.length==0){
				return;
			}
			var internalChain:InternalChain = new InternalChain();
			internalChain.dispatcher = this;
			internalChain.startInterval = this.startInterval;
			internalChain.taskToTaskInterval = this.taskToTaskInterval;
			internalChain.concurrent = this.concurrent;
			var copyList:Array = new Array();
			for(var i:int = 0,size:int=taskList.length;i<size;i++){
				copyList[i] = taskList[i];
			}
			internalChain.taskList = copyList;
			internalChain.execute();
		}
		public function stop():void{
		}
	}
}

import flash.utils.Dictionary;
import flash.utils.setTimeout;
import asphalt2.util.Async;
import flash.events.IEventDispatcher;
import flash.events.Event;
	
class InternalChain{
	public var dispatcher:IEventDispatcher;
	public var startInterval:uint;
	public var taskToTaskInterval:uint;
	public var concurrent:Boolean;
	//実行するタスクのリスト
	public var taskList:Array;
	//並列実行時のタスクインデックス
	private var nextTaskIndex:uint = 0;
	//終了したタスクの数
	private var endTaskNum:uint = 0;
	/**
	 * タスクの実行
	 */
	public function execute():void{
		if(taskList.length==0){
			return;
		}
		if(concurrent){
			setTimeout(concurrentExecute,startInterval);
		}else{
			setTimeout(orderExecute,startInterval);
		}
	}
	/**
	 * タスクの停止
	 */
	public function stop():void{
		nextTaskIndex = taskList.length;
		endChain();
	}
	
	private function concurrentExecute():void{
		executeFunction(taskList[nextTaskIndex++]);
		if(nextTaskIndex < taskList.length){
			setTimeout(concurrentExecute,taskToTaskInterval);
		}
	}
	private function orderExecute():void{			
		if(nextTaskIndex < taskList.length){
			executeFunction(taskList[nextTaskIndex++]);
		}			
	}
	private function executeFunction(func:Function):void{			
		try{
			try{
				var result:* = func();
				endFunction(result);
				return;
			}catch(ar:ArgumentError){
				try{
					func(new Async(endFunction));
					return;
				}catch(ara:TypeError){
					func(endFunction);
					return;
				}
			}
		}catch(e:Error){
			stop();
			throw e;
		}
	}
	private function endFunction(result:*=null):void{
		endTask();
		if(concurrent==false){
			setTimeout(orderExecute,taskToTaskInterval);
		}
	}
	private function endTask():void{
		if(++endTaskNum>=taskList.length){
			endChain();
		}
	}
	/**
	 * すべてのタスクが終了したときか、タスク実行時にエラーがあった場合に呼び出されます。
	 * Event.COMPLETEをdispatchします。
	 **/
	private function endChain():void{
		dispatcher.dispatchEvent(new Event("complete"));
	}
}
